Sangria + Akka HTTPでGraphQL Connection編
こんにちは、かたいなかです。
前回に引き続き、GraphQLのScalaでの実装であるSangriaについて解説していきます。
今回はページネーションを行うAPIを作成する方法について解説します。
Connection
今回はRelayのCursor Connectionという仕様に従ってページネーションを実装します。 まずは今回実装していくAPIを例にConnectionがどのようなものかを見ていきましょう。
クエリおよびレスポンスは以下のような形式です。
edges
やnode
,cursor
といった見慣れない名前がありますね。
順番に解説します。
クエリの引数
articles
のようなConnection型を返すフィールドはページネーションに関連した引数を取れるようにします。
前方にページを読み進めるための属性
after
この引数で指定されたカーソルより後ろの要素を返す。first
afterで指定されたカーソルから(指定されていない場合は最初から)この引数で指定された個数分の要素を返す。
後方にページを戻るための属性
before
この引数で指定されたカーソルより前の要素を返す。last
beforeで指定されたカーソルから逆向きにこの引数で指定された個数分の要素を返す。
上の画像の例では、after
を指定せずにfirst
に3
という値が指定されているため、最初の要素から3つを取得しています。
Connection
articles
フィールドはArticleConnection
という型を持ちます。
属性
edges
ページ内の要素をカーソルの情報とまとめたEdge型の配列。pageInfo
今回のレスポンスのページ情報。PageInfo型。
Edge
articles
がもつedges
はEdge型の配列です。
属性
node
検索対象のオブジェクトcursor
ページネーションで使用するカーソル
PageInfo
PageInfoは、今回レスポンスとして返した要素のページの情報を返します。
属性
hasNextPage
次のページが存在するかhasPreviousPage
前のページが存在するかendCursor
edgesの最後の要素がもつカーソルstartCursor
edgesの最初の要素のカーソル
実装
今回もSangriaを使用して実装していきます。Akka HTTPとつなぎ込んだりする部分は前回と同様なので今回の記事中では触れません。
今回は以下のようにクエリが実行できるようにします。
また、以下のように記事に関連するコメントも検索できるようにします。
依存ライブラリ
build.sbt
の内容は以下のとおりです。
前回のブログのものにsangria-relay
というライブラリへの依存を追加しました。
name := "sangria-example" version := "0.1" scalaVersion := "2.12.5" libraryDependencies ++= Seq( "org.sangria-graphql" %% "sangria" % "1.4.0", "org.sangria-graphql" %% "sangria-circe" % "1.2.1", "org.sangria-graphql" %% "sangria-relay" % "1.4.0", "com.typesafe.akka" %% "akka-http" % "10.1.0", "de.heikoseeberger" %% "akka-http-circe" % "1.20.0", "io.circe" %% "circe-core" % "0.9.2", "io.circe" %% "circe-parser" % "0.9.2", "io.circe" %% "circe-optics" % "0.9.2", )
実際にデータを取得する
Schemaから呼び出されるページネーションされたデータを取得するメソッドを実装します。
//メモリ内のコレクションを使用した実装 //実際にはafter,before,first,lastを使用してDB等にどのようなクエリでアクセスするかを決定しデータを取得 //データが取得できたらページング情報とともにデータを返すようにする def articleConnection(connectionArgs: ConnectionArgs): Connection[Article] = Connection.connectionFromSeq(articles, connectionArgs) def commentConnection(articleId: String, connectionArgs: ConnectionArgs): Connection[Comment] = Connection.connectionFromSeq(comments.filter(_.articleId == articleId), connectionArgs)
今回はsangria-relay
内で用意されているConnection.connectionFromSeq
というメソッドを使用してメモリ内のコレクションを用いた簡単な実装を作っています。
実際の開発では、クエリの引数として受け取るafter
,before
,first
,last
の4つの値を使用してDB等から要素を取得し、ページング用の情報と合わせて結果を返すように実装することになるでしょう。
この部分についてMongoDBを使用した例として以下のコードが参考になります。
スキーマの実装
import sangria.relay._ import sangria.schema._ object SchemaDefinition { // GraphQL上のComment型を定義しています。 val CommentType = ObjectType( "Comment", "コメント", fields[ArticleRepository, Comment]( Field("id", StringType, Some("コメントのId"), resolve = _.value.id), Field("articleId", StringType, Some("コメントがついている記事のId"), resolve = _.value.articleId), Field("body", StringType, Some("コメントの本文"), resolve = _.value.body) ) ) //GraphQL上でのCommentConnection型を定義します val ConnectionDefinition(_, commentConnection) = Connection.definition[ArticleRepository, Connection, Comment]("Comments", CommentType) // GraphQL上のArticle型を定義しています。 val ArticleType = ObjectType( "Article", "記事", fields[ArticleRepository, Article]( Field("id", StringType, Some("記事のId"), // Scalaコード中のArticle型とのマッピングを記述しています(_.valueはArticle型) resolve = _.value.id), Field("title", StringType, Some("記事のタイトル"), resolve = _.value.title), Field("author", OptionType(StringType), Some("記事の著者"), resolve = _.value.author ), // commentsという要素で関連するCommentを取得するようにしています。 Field("comments", OptionType(commentConnection), Some("記事についているコメント"), arguments = Connection.Args.All, resolve = ctx => ctx.ctx.commentConnection(ctx.value.id, ConnectionArgs(ctx))), Field("tags", ListType(StringType), Some("記事についているタグ"), resolve = _.value.tags) )) // このように記述することもできます // import sangria.macros.derive._ // val ArticleType = deriveObjectType[ArticleRepository, Article]( // AddFields( // Field( // "comments", // OptionType(commentsConnection), // Some("記事についているコメント"), // arguments = Connection.Args.All, // resolve = // ctx => ctx.ctx.commentsConnection(ctx.value.id, ConnectionArgs(ctx)) // ), // ) // ) //GraphQL上でのArticleConnection型を定義します val ConnectionDefinition(_, articleConnection) = Connection.definition[ArticleRepository, Connection, Article]("Article", ArticleType) // Queryに使用する引数です これはString型のidという名前を持つ引数 val idArgument = Argument("id", StringType, description = "id") // Queryです。ここにクエリ操作を定義していきます。 val QueryType = ObjectType( "Query", fields[ArticleRepository, Unit]( Field( "article", OptionType(ArticleType), arguments = idArgument :: Nil, // ArticleRepositoryからどのように記事を取得するかを記述しています(ctx.ctxはArticleRepository型) resolve = ctx => ctx.ctx.findArticleById(ctx.arg(idArgument))), Field("articles", OptionType(articleConnection), arguments = Connection.Args.All, resolve = ctx => ctx.ctx.articleConnection(ConnectionArgs(ctx))) ) ) // 最後にSchemaを定義します。 val ArticleSchema = Schema(QueryType) }
上が今回のSchema定義のコードです。多くが前回の記事での内容と同一のため、重要な部分について解説します。
上記のコード中で記事の一覧をページネーションに対応させるのに重要な部分は以下です。
まず、GraphQLのArticleConnection
型を定義します。
val ConnectionDefinition(_, articleConnection) = Connection.definition[ArticleRepository, Connection, Article]("Article", ArticleType)
クエリの値を取得する際には、ConnectionArgs(ctx)
とすることで、クエリで受け取ったafter
,before
,first
,last
の4つの属性をもつConnectionArgs
型のオブジェクトを作成し、Connectionオブジェクトを取得するメソッドに渡しています。
Field("articles", OptionType(articleConnection), arguments = Connection.Args.All, resolve = ctx => ctx.ctx.articleConnection(ConnectionArgs(ctx)))
また、関連するコメントを取得するために重要な部分は以下です。
まず、上のArticleConnection
と同様に、GraphQLのCommentsConnection
型を定義します。
val ConnectionDefinition(_, commentConnection) = Connection.definition[ArticleRepository, Connection, Comment]("Comments", CommentType)
ある記事に関連するコメントとして取得できるようにするため、ArticleType
内のフィールドとして定義します。ctx.value.id
により記事のIDを取得し、取得したIDを用いて記事に関連するコメントを取得しています。
// commentsという要素で関連するCommentを取得するようにしています。 Field("comments", OptionType(commentConnection), Some("記事についているコメント"), arguments = Connection.Args.All, resolve = ctx => ctx.ctx.commentConnection(ctx.value.id, ConnectionArgs(ctx))),
実際に実行してみる
ここまでで実装は完了したので、実際に実行して動作を確認してみましょう。
まずは記事の一覧のページネーションを試してみます。
hasNextPage
がtrue
となっているので、次のページがあるようです。そこで、endCursor
で返ってきた値をafter
に指定して次のページを取得してみましょう。
期待通りにページネーションされたデータを取得することができました。
同様に、ある記事に関連するコメントの情報も取得します。
こちらも期待通りにデータを取得することができました。
まとめ
今回はSangriaでRelayのConnectionの仕様に基づいてページネーションに対応したGraphQLのAPIを実装する方法を紹介しました。
今回の実装
https://github.com/katainaka0503/hello-sangria
参考資料
- graphql.orgのPaginationのページ
- Relay Cursor Connections Specification
- Github GraphQL API v4
- Take a sip of Sangria Relay